#region using

using System;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using System.Web;
using System.IO;
using System.Text;
using System.Collections.Generic;

#endregion

/// <summary>
/// Converts ASCII smilies into real smilies in the comments.
/// </summary>
/// <remarks>
/// More fun in store
/// </remarks>
[Extension("Improves post typography", "1.0", "<a href=\"http://nesteruk.org\">Dmitri Nesteruk</a>")]
public class TypograFix
{
  /// <summary>
  /// Listen to posts being served.
  /// </summary>
  static TypograFix()
  {
    Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
  }

  /// <summary>
  /// The event handler that is triggered every time a comment is served to a client.
  /// </summary>
  /// <remarks>
  /// Exceptions are handled lazily at the moment.
  /// </remarks>
  private static void Post_Serving(object sender, ServingEventArgs e)
  {
    if (!string.IsNullOrEmpty(e.Body))
    {
      try
      {
        e.Body = Transform(HttpUtility.HtmlDecode(e.Body))
                 // + "<pre>" + HttpUtility.HtmlEncode(HttpUtility.HtmlDecode(e.Body)) + "</pre>"
                 ;
      }
      catch (Exception ex)
      {
        // todo: report when admin
        e.Body += " Exception: " + ex.Message;
      }
    }
  }

  #region Production
  /// <summary>
  /// The tags in which text cannot be transformed.
  /// </summary>
  private static readonly string[] unsuitable = { "code", "pre" };

  /// <summary>
  /// This stack stores a list of tag we're in.
  /// </summary>
  private static readonly Stack<string> context = new Stack<string>();

  /// <summary>
  /// Returns <c>true</c> if replacement is forbidden in given context.
  /// </summary>
  /// <value><c>true</c> if cannot replace; otherwise, <c>false</c>.</value>
  private static bool cannotReplace
  {
    get
    {
      // code or pre elements get banned
      foreach (string s in context)
      { // can't even use LINQ :(
        if (Array.IndexOf(unsuitable, s.ToLower()) >= 0)
          return true;
      }
      return false;
    }
  }

  /// <summary>
  /// This function actually alters the text and makes the necessary changes.
  /// </summary>
  /// <param name="text">Initial text, with HTML entities decoded (use <see cref="T:System.Web.HttpUtility"/>.</param>
  /// <returns>The altered text.</returns>
  private static string Transform(string text)
  {
    StringBuilder sb = new StringBuilder(text.Length);
    Stack<char> quoteStack = new Stack<char>();
    for (int i = 0; i < text.Length; ++i)
    {
      switch (text[i])
      {
        #region <
        case '<':
          sb.Append('<');
          StringBuilder tb = new StringBuilder();
          while (++i < text.Length)
          {
            sb.Append(text[i]);
            if (char.IsWhiteSpace(text[i]))
            {
              // we're in this tag's context
              context.Push(tb.ToString());
            }
            else if (text[i] == '/')
            {
              tb = new StringBuilder();
              context.Pop();
              // move until we meet > and add it
              while (++i < text.Length)
              {
                if (text[i] == '>')
                  goto default;
                sb.Append(text[i]);
              }
            }
            else if (text[i] == '>')
            {
              // if closed, break this loop
              context.Push(tb.ToString());
              break;
            }
            else
              tb.Append(text[i]);
          };
          break;
        #endregion

        #region -
        case '-':
          if (cannotReplace) goto default;
          // if surrounded by spaces, it's an en dash
          if (i > 0 && i + 1 < text.Length && text[i - 1] == ' ' && text[i + 1] == ' ')
            sb.Append("&ndash;");
          else if (i + 1 < text.Length && text[i + 1] == '-')
          {
            sb.Append("&mdash;");
            ++i; // ignore the second dash
          }
          else goto default;
          // same for em dash
          break;
        #endregion

        #region (
        case '(':
          if (cannotReplace) goto default;
          if (i + 2 < text.Length && text[i + 2] == ')')
          {
            if (char.ToLower(text[i + 1]) == 'c')
            {
              sb.Append("&copy;");
              i += 2;
            }
            else if (char.ToLower(text[i + 1]) == 'r')
            {
              sb.Append("&reg;");
              i += 2;
            }
            else goto default;
          }
          else if (i + 3 < text.Length && char.ToLower(text[i + 1]) == 't'
                                       && char.ToLower(text[i + 2]) == 'm'
                                      && char.ToLower(text[i + 3]) == ')')
          {
            sb.Append("&#8482;");
            i += 3;
          }
          else goto default;
          break;
        #endregion

        #region '
        case '\'': // single quotes
          if (cannotReplace) goto default;

          // if there's no space on the right, force closing
          if (i > 0 && i + 1 < text.Length
                    && (char.IsLetterOrDigit(text[i - 1]))
                    && (char.IsLetterOrDigit(text[i + 1])))
          {
            sb.Append("&rsquo;"); // stack ignored
          }
          else if (quoteStack.Count > 0 && quoteStack.Peek() == '\'')
          {
            sb.Append("&rsquo;");
            quoteStack.Pop();
          }
          else
          {
            sb.Append("&lsquo;");
            quoteStack.Push('\'');
          }
          break;
        #endregion

        #region "
        case '\"': // single quotes
          if (cannotReplace) goto default;
          // if there is a quote on the stack, close it
          if (quoteStack.Count > 0 && quoteStack.Peek() == '\"')
          {
            sb.Append("&rdquo;");
            quoteStack.Pop();
          }
          else
          {
            sb.Append("&ldquo;");
            quoteStack.Push('\"');
          }
          break;
        #endregion

        #region .
        case '.': // possible ellipsis
          if (cannotReplace) goto default;
          if (i + 2 < text.Length && text[i + 1] == '.' && text[i + 2] == '.')
          {
            if (i > 0 && i + 3 < text.Length)
            {
              char before = text[i - 1];
              char after = text[i + 3];
              if (Environment.NewLine.IndexOf(before) >= 0 &&
                Environment.NewLine.IndexOf(after) >= 0)
              {
                sb.Append("<br/>&#8942;<br/>");
                i += 2;
                continue;
              }
            }
            sb.Append("&hellip;");
            i += 2;
          }
          else goto default;
          break;
        #endregion

        #region x
        case 'x':
          if (cannotReplace) goto default;
          if (i > 0 && i + 1 < text.Length
                    && char.IsDigit(text[i - 1])
                    && (char.IsDigit(text[i + 1]) || char.IsWhiteSpace(text[i + 1])))
          {
            sb.Append("&times;");
          }
          else goto default;
          break;
        #endregion

        default:
          sb.Append(text[i]);
          break;
      }
    }
    return sb.ToString();
  }
  #endregion
}